package ganga.custodian;

import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Set;
import java.util.UUID;

/**
 * Класс инкапсулирует всю логику работы с Bluetooth
 * 
 * @author Apocrypha
 * @version 0.4
 * @updated 09-сен-2011 19:34:43
 */
public class BluetoothManager {

	private CustodianApplication application;

	private static final int READ_BUFFER_SIZE = 1024;
	private static final String NO_BLUETOOTH_HARDWARE = "Отсутствует устройство Bluetooth";
	private static final String SECURITY_PROBLEM = "Приложению не предоставлены права доступа к Bluetooth";

	/**
	 * Constants that indicate the current connection state
	 * @author Apocrypha
	 *
	 */
	public class State{
		/**
		 * we are starting
		 */
		public static final int INITIALIZING = 0;
		
	}
	public static final int STATE_INITIALIZING = 0; //we are starting
	public static final int STATE_IDLE = 1; // we're doing nothing
	public static final int STATE_LISTEN = 2; // now listening for incoming connections
	public static final int STATE_CONNECTING = 3; // now initiating an outgoing connection
	public static final int STATE_CONNECTED = 4; // now connected to a remote device

	public class BluetoohException extends Exception {

		public BluetoohException(String message, Exception ex) {
			super(ex);
			this.initCause(ex);
		}

		public BluetoohException(String message) {
			super(message);
		}

		/**
		 * serialVersionUID используется для нужд серриализации, а точнее - для
		 * определения версии класса при дессериализации объектов
		 */
		private static final long serialVersionUID = 8920611772378371657L;

	}

	public BluetoothManager(Activity activity) throws BluetoohException {
		application = CustodianApplication.getInstance();
		this.activity = activity;
		initialize();
		handler = new Handler() {
			@Override
			public void handleMessage(Message msg) {
				switch (msg.what) {
				case BluetoothManager.MESSAGE_STATE_CHANGE:
					if (CustodianApplication.DEBUG)
						Log.i(TAG, "MESSAGE_STATE_CHANGE: " + msg.arg1);
					switch (msg.arg1) {
					case BluetoothManager.STATE_CONNECTED:
						// setStatus(getString(R.string.title_connected_to,
						// mConnectedDeviceName));
						// mConversationArrayAdapter.clear();
						break;
					case BluetoothManager.STATE_CONNECTING:
						// setStatus(R.string.title_connecting);
						break;
					case BluetoothManager.STATE_LISTEN:
					case BluetoothManager.STATE_IDLE:
						// setStatus(R.string.title_not_connected);
						break;
					}
					break;
				case BluetoothManager.MESSAGE_WRITE:
					byte[] writeBuf = (byte[]) msg.obj;
					// construct a string from the buffer
					// String writeMessage = new String(writeBuf);
					// mConversationArrayAdapter.add("Me:  " + writeMessage);
					break;
				case BluetoothManager.MESSAGE_READ:
					int size = msg.arg1;
					byte[] readBuf = (byte[]) msg.obj;
					for (int i = 0; i < size; i+=2) {
						byte b1 = readBuf[i];
						byte b2 = readBuf[i+1];
						int result = b1 * 256 + b2;
						application.cardiogram.RRIntervals.add(result);
					}
					application.cardiogram.notifyObservers();
					break;
				// case MESSAGE_DEVICE_NAME:
				// // save the connected device's name
				// mConnectedDeviceName = msg.getData().getString(DEVICE_NAME);
				// Toast.makeText(getApplicationContext(), "Connected to "
				// + mConnectedDeviceName, Toast.LENGTH_SHORT).show();
				// break;
				case BluetoothManager.MESSAGE_TOAST:
					application.showToast(msg.getData().getString(TOAST));
					break;
				}
			}// handleMessage
		};

		inputStream = null;
	}

	// /////////////////////////////////////////////////////////////////////////
	private static final String TAG = "BluetoothManager";
	public static BluetoothAdapter adapter;
	private Activity activity;
	private boolean enabledBefore = true;
	static final int ENABLE_BT_REQUEST = 3245;
	private static final boolean requestUserPermission = false;

	// TODO access modifcators = ?
	protected static final int MESSAGE_STATE_CHANGE = 0;
	protected static final int MESSAGE_READ = 1;
	protected static final int MESSAGE_WRITE = 2;
	protected static final int MESSAGE_DEVICE_NAME = 4;
	protected static final int MESSAGE_TOAST = 5;

	public static final String TOAST = "toast";
    public static final String DEVICE_NAME = "device_name";

	// Name for the SDP record when creating server socket
    private static final String NAME_SECURE = "BluetoothChatSecure";
    //private static final String NAME_INSECURE = "BluetoothChatInsecure";

    // Unique UUID for this application
    private static final UUID MY_UUID_SECURE =
        //UUID.fromString("fa87c0d0-afac-11de-8a39-0800200c9a66");
    	UUID.fromString("00001402-0000-1000-8000-00805F9B34FB");

	public static final boolean USE_REFLECTION = false;
    private BluetoothAdapter initialize() throws BluetoohException {
		adapter = BluetoothAdapter.getDefaultAdapter();
		if (adapter == null)
			throw new BluetoohException(NO_BLUETOOTH_HARDWARE);
		try {
			if (adapter.isEnabled()) {
				enabledBefore = true;
			} else {
				enabledBefore = false;
				if (requestUserPermission)
					activity.startActivityForResult(new Intent(
							BluetoothAdapter.ACTION_REQUEST_ENABLE),
							BluetoothManager.ENABLE_BT_REQUEST);
				adapter.enable();
			}
		} catch (SecurityException ex) {
			throw new BluetoohException(SECURITY_PROBLEM, ex);
		}
		return adapter;
	}

	/**
	 * Заполняет указанные массивы именами и адресами устройств
	 * 
	 * @param names
	 * @param addresses
	 */
	public void populateDevicesList(ArrayList<String> names,
			ArrayList<String> addresses) {
		Set<BluetoothDevice> pairedDevices = adapter.getBondedDevices();
		for (BluetoothDevice bluetoothDevice : pairedDevices) {
			names.add(bluetoothDevice.getName());
			addresses.add(bluetoothDevice.getAddress());
		}
	}

	// ////////////////////////////////////////////////////////////////////////////////////////
	private InputStream  inputStream;	
	private OutputStream outputStream;
    
    /**
     * Used by Connector
     */
    //private final BluetoothSocket mmSocket;
	/**
	 * The local server socket
	 */
	private BluetoothServerSocket aServerSocket;
  

	/**
	 * The Handler that gets information back from the caller
	 */
	private final Handler handler;
 	private Listener secureListener;
 	//private Listener insecureListener;
    private volatile Connector connector;
    private volatile DataTransmitter dataTransmitter;

	private int currentState;
    //////////////////////////////////////////////////////////
    /**
     * Set the current state of the chat connection
     * @param state  An integer defining the current connection state
     */
    private synchronized void setState(int state) {
        if (CustodianApplication.DEBUG) Log.d(TAG, "setState() " + currentState + " -> " + state);
        currentState = state;

        // Give the new state to the Handler
        handler.obtainMessage(MESSAGE_STATE_CHANGE, state, -1).sendToTarget();
    }

    /**
     * Возвращает текущее состояние менеджера
     * Return the current connection state. 
     */
    public synchronized int getState() {
        return currentState;
    }

	// ///////////////////////////////////////////////////////
	/**
	 * Start the bluetooth service. Specifically start Listener 
	 * to begin a session in listening (server) mode.
	 */
	public synchronized void start() {
		if (CustodianApplication.DEBUG)	Log.d(TAG, "start");

		cancelConnector();		  // Cancel any thread attempting to make a connection
		cancelDataTransmitter();  // Cancel any thread currently running a connection

		setState(STATE_LISTEN);

		startListner();
		
		/* For API >=10
 		if (secureListener == null) {
			secureListener = new Listener(false);
			secureListener.start();
		}
		*/
	}
	public synchronized void stop() {
		cancelConnector();
		cancelListener();
		cancelDataTransmitter();
		setState(STATE_IDLE);
	}

	/**
	 * Запустить поток слушающий BluetoothServerSocket
	 */
	private void startListner() {
		if (secureListener == null) {
			secureListener = new Listener(true);
			secureListener.start();
		}
	}

	/**
	 * Cancel the thread that completed the connection
	 */
	private void cancelConnector() {
		if (connector==null)
			return;
		cancelThread(connector);
		connector.interrupt();
		connector = null;
	}
	private void cancelListener() {
		cancelThread(secureListener);
		secureListener = null;
	}
	/**
	 * Cancel any thread currently running a connection
	 */
	private void cancelDataTransmitter() {
		cancelThread(dataTransmitter);
		dataTransmitter = null;
	}
	private void cancelThread(ICanceled thread) {
		if (thread != null) {
			thread.cancel();
		}
	}
    /**
     * Listen to the server socket if we're not connected
     */
    private void listen() {
        BluetoothSocket socket = null;

        // Listen to the server socket if we're not connected
        while (currentState != STATE_CONNECTED) {
            try {
                // This is a blocking call and will only return on a
                // successful connection or an exception
                socket = aServerSocket.accept();
            } catch (IOException e) {
                Log.e(TAG, "Socket accept() failed", e);
                break;
            }

			if (socket == null)
				continue;

			// If a connection was accepted
			synchronized (this) {
				switch (currentState) {
				case STATE_LISTEN:
				case STATE_CONNECTING:
					// Situation normal. Start the connected thread.
					connected(socket);
					break;
				case STATE_IDLE:
				case STATE_CONNECTED:
					// Either not ready or already connected. Terminate new
					// socket.
					try {
						socket.close();
					} catch (IOException e) {
						Log.e(TAG, "Could not close unwanted socket", e);
					}
					break;
				}
			}// sync
		}//currentState != STATE_CONNECTED
	}
    /**
     * Start the ConnectThread to initiate a connection to a remote device.
     * @param device  The BluetoothDevice to connect
     * @param secure Socket Security type - Secure (true) , Insecure (false)
     */
	public synchronized void connect(String deviceAddress) {

		cancelConnector();// Cancel any thread attempting to make a connection
		cancelDataTransmitter();// Cancel any thread currently running a connection

		// Start the thread to connect with the given device
		try {
			// Get the BluetoothDevice object
			BluetoothDevice device = adapter.getRemoteDevice(deviceAddress);
			if (CustodianApplication.DEBUG)
				Log.d(TAG, "connect to: " + device);

			connector = new Connector(device);
			connector.start();
			setState(STATE_CONNECTING);
		} catch (IllegalArgumentException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (NoSuchMethodException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

    /**
     * Start the ConnectedThread to begin managing a Bluetooth connection
     * @param socket  The BluetoothSocket on which the connection was made
     */
    public synchronized void connected(BluetoothSocket socket) {
        if (CustodianApplication.DEBUG) Log.d(TAG, " connected");

        cancelConnector();
        cancelDataTransmitter();
        cancelListener();// Cancel the accept thread because we only want to connect to one device
 
        // Start the thread to manage the connection and perform transmissions
        dataTransmitter  = new DataTransmitter(socket);
        dataTransmitter.start();

        String deviceName = socket.getRemoteDevice().getName();
        sendDeviceNameToObserver(deviceName);

        setState(STATE_CONNECTED);
    }

	/**
	 * Send the name of the connected device back to observer
	 * @param deviceName 
	 */
	private void sendDeviceNameToObserver(String deviceName) {
		Message msg = handler.obtainMessage(MESSAGE_DEVICE_NAME);
        Bundle bundle = new Bundle();
        bundle.putString(DEVICE_NAME, deviceName);
        msg.setData(bundle);
        handler.sendMessage(msg);
	}

	/**
	 * Indicate that the connection was lost and notify the observer.
	 */
	private void connectionLost(String message) {
		// Send a failure message back to the Observer
		Message msg = handler.obtainMessage(MESSAGE_TOAST);
		Bundle bundle = new Bundle();
		bundle.putString(TOAST, message);
		msg.setData(bundle);
		handler.sendMessage(msg);
	}
	
	interface ICanceled{
		public void cancel();
	}//ICanceled

	/**
     * This thread runs while listening for incoming connections. It behaves
     * like a server-side client. It runs until a connection is accepted
     * (or until canceled).
     */
	private class Listener extends Thread implements ICanceled{
	  
		public Listener(boolean secure){
			setName("Bluetooth.Listner");
            socketType = secure ? "Secure":"Insecure";

            // Create a new listening server socket
            try {
                if (secure) {
                    aServerSocket = adapter.listenUsingRfcommWithServiceRecord(NAME_SECURE,
                        MY_UUID_SECURE);
                } else {
                	//Not avaible for API<10
					//aServerSocket = adapter.listenUsingInsecureRfcommWithServiceRecord(
					//				NAME_INSECURE, MY_UUID_INSECURE);
				}
            } catch (IOException e) {
                Log.e(TAG, "Socket Type: " + socketType + "listen() failed", e);
            }
		}
		public void run() {
			 if (CustodianApplication.DEBUG) Log.d(TAG, "Socket Type: " + socketType +
	                    "BEGIN mAcceptThread" + this);
			 listen();
	         if (CustodianApplication.DEBUG) Log.i(TAG, "END Listner");

		}
        private String socketType;
		@Override
		public void cancel() {
            if (CustodianApplication.DEBUG) Log.d(TAG, "Socket Type" + socketType + "cancel " + this);
            try {
            	aServerSocket.close();
            } catch (IOException e) {
                Log.e(TAG, "Socket Type" + socketType + "close() of server failed", e);
            }			
		}
	}//Listener

	/**
	 * It runs while attempting to make an outgoing connection with a device.
	 * This thread runs straight through; the connection either succeeds or
	 * fails.
	 */
	private class Connector extends Thread implements ICanceled {
		//BluetoothDevice device;
		private BluetoothSocket _socket;

		public Connector(BluetoothDevice device) throws NoSuchMethodException, IllegalArgumentException,
				IllegalAccessException, InvocationTargetException {
			//this.device=device;
			setName("Bluetooth.Connector");
			
			try {
				if (USE_REFLECTION){
					Method m;
					m = device.getClass().getMethod("createRfcommSocket", new Class[]{int.class});
					_socket = (BluetoothSocket)m.invoke(device, Integer.valueOf(1)); 
				}
				else{
					_socket = device.createRfcommSocketToServiceRecord(MY_UUID_SECURE);
				}
			} catch (IOException ex) {
				Log.e(TAG, "Socket creation faild "+ex.getMessage());
				ex.printStackTrace();
			}
		}

		public void run() {
			// Always cancel discovery because it will slow down a connection
			adapter.cancelDiscovery();
			// Make a connection to the BluetoothSocket
			try {
				// This is a blocking call and will only return on a
				// successful connection or an exception
				_socket.connect();
				Log.v(TAG, "Connect by"+ _socket.toString());
				//application.showToast("Connect by"+ _socket.toString());
			} catch (IOException e) {
				// Close the socket
				try {
					_socket.close();
				} catch (IOException e2) {
					Log.e(TAG, "unable to close() "
							+ " socket during connection failure", e2);
				}
				connectionLost("Unable to connect device \n"+e.getMessage());
				return;
			} 

			// Reset the ConnectThread because we're done
			synchronized (this) {
				connector = null;
			}
			connected(_socket);
		}

		public void cancel() {
			if (CustodianApplication.DEBUG)
				Log.d(TAG, "Socket cancel " + this);
			try {
				_socket.close();
			} catch (IOException e) {
				Log.e(TAG, "Socket close() of connent failed", e);
			}

		}
	}// Connector
 
	/**
	 * This thread runs during a connection with a remote device. It handles all
	 * incoming and outgoing transmissions.
	 */
	private class DataTransmitter extends Thread implements ICanceled {
		public DataTransmitter(BluetoothSocket socket){
			setName("Bluetooth.DataTransmitter");
			this.socket = socket;

			// Get the BluetoothSocket input and output streams
            try {
                inputStream  = socket.getInputStream();
                outputStream = socket.getOutputStream();
            } catch (IOException e) {
                Log.e(TAG, "Streams not created", e);
            }
		}
		public void run(){
			Log.i(TAG, "BEGIN Read");
			byte[] buffer = new byte[READ_BUFFER_SIZE];
			int readedBytes;
			
			// Keep listening to the InputStream while connected
			Thread thisThread = Thread.currentThread();
			while (thisThread==dataTransmitter) {
				try {
					// Read from the InputStream
					readedBytes = inputStream.read(buffer);
			
					// Send the obtained bytes to the handler queue
					handler.obtainMessage(MESSAGE_READ, readedBytes, -1, buffer)
							.sendToTarget();
			
				} catch (IOException ex) {
					Log.e(TAG, "disconnected", ex);
					connectionLost("Device connection was lost");// ?
					// Start the service over to restart listening mode
					cancelConnector();
					start();// ?
					break;
				}
			}
		}
		@Override
		public void cancel() {
			try {
				socket.close();
			} catch (IOException e) {
				Log.e(TAG, "close() of connect socket failed", e);
			}
		}
		BluetoothSocket socket;
	}// DataTransmitter
}//BluetoothManager
